#!/bin/bash

# Copyright 2021, soneca e cv8minix3.
#
# soneca acenos@danwin1210.me 
# cv8minix3 terroristcv8@keemail.me
# 
# album-dl-advanced é um software livre; você pode redistribuí-lo e/ou
# modificá-lo sob os termos da Licença Pública Geral GNU como publicada
# pela Free Software Foundation; na versão 3 da Licença, ou
# (a seu critério) qualquer versão posterior.
#
# Este programa é distribuído na esperança de que possa ser útil,
# mas SEM NENHUMA GARANTIA; sem uma garantia implícita de ADEQUAÇÃO
# a qualquer MERCADO ou APLICAÇÃO EM PARTICULAR. Veja a
# Licença Pública Geral GNU para mais detalhes.
#
# Você deve ter recebido uma cópia da Licença Pública Geral GNU junto
# com este programa. Se não, veja <http://www.gnu.org/licenses/>.

## Formatação de saída
INTE="[\e[33mINTE\e[0m]"
INFO="[\e[32mINFO\e[0m]"
ERRO="[\e[31mERRO\e[0m]"

## Variáveis úteis
ua="Mozilla/5.0 (Android 7.0; Mobile; rv:54.0) Gecko/54.0 Firefox/54.0"
headers='--header="Accept-Language: pt-BR,pt" --header="Accept-Encoding: identity"'

# Tentativa de padronizar ambiente.
export LANG=pt_BR.UTF-8

# Conversões/traduções de gêneros comuns (original|gringo).
DICIO_GEN="s/poprock/pop-rock/g; s/jovem-guarda/samba-rock-pop-other/g; s/pagode/samba/g; \
s/mpb/samba-acoustic-pop-other/g; s/sertanejo/country-pop-acoustic-other/g; \
s/gospelreligioso/gospel/g; s/romantico/pop-other/g; s/gotico/pop-rock-punk-dream-other/g; \
s/velha-guarda/samba-rock-pop-other/g;"

# A lista de gêneros padrão (se não existe, será gerada).
LISTA_GEN=/tmp/lista_gen.txt

## Imprime em tela os padrões de uso.
function USO(){
	echo -e "Uso: $0 \e[92m[AÇÃO]\e[0m \e[33m[OPÇÃO]\e[0m"
	echo -e "\t\e[92m[-d|--dl|--download]\e[0m \e[33m[URL] [QUALIDADE]\e[0m	:\e[94mBaixa álbum.\e[0m"
	echo -e "\t\e[92m[-f|--fix|--fixname]\e[0m \e[33m[STRING]\e[0m		:\e[94mPadroniza nomes dos arquivos e remove string especificada.\e[0m"
	echo -e "\t\e[92m[-s|--set|--set-tags]\e[0m \e[33m[URL_METADADOS]\e[0m	:\e[94mInsere metadados nos arquivos.\e[0m"
	echo -e "\t\e[92m[-v|--show|--show-tags]\e[0m			:\e[94mMostra metadados dos arquivos.\e[0m"
	echo -e "\t\e[92m[-r|--rn|--renumera]\e[0m			:\e[94mRenumera arquivos fora da ordem do álbum.\e[0m"
	exit 1;	
}

# Checa dependências para execução do script.
function DEP_CHECK(){
	# Todos os comandos externos de que o script depende.
	local IFS=' ' cmd libs="id3v2 youtube-dl ffmpeg sed egrep wget sort uniq ls mv cat rev cut rm iconv"
	for cmd in $libs
		do
			type -tf $cmd &> /dev/null; test $? -ne 0 && echo -e "$ERRO \"$cmd\" não instalado!" && exit 2
	done
}

# Checa existência de arquivos mp3.
function MP3_CHECK(){
	local arqs IFS=$'\n'; arqs=$( echo *.mp3 ); test "$arqs" = '*.mp3' && \
	echo -e "$ERRO Não existem arquivos .mp3 neste diretório!" && exit 2
}

# Gera lista de gêneros padrão ID3, caso não exista.
function GERA_LISTA_GENEROS(){
	# Para evitar guardar os gêneros aqui, são 30 linhas.
	local generos_lista="https://en.wikipedia.org/wiki/List_of_ID3v1_Genres"

	# Para padronizar em qualquer ambiente.
	local ua="Mozilla/5.0 (Android 7.0; Mobile; rv:54.0) Gecko/54.0 Firefox/54.0"
	local headers='--header="Accept-Language: pt-BR,pt" --header="Accept-Encoding: identity"'

	# Apenas se não existe.
	test -f $LISTA_GEN && return 0

	# Baixa HTML.
	local retorno=2
	while [ "$retorno" -ne "0" ]
	do
		generos_lista=$( wget -q --user-agent="$ua" "$headers" "$generos_lista" -O - )
		retorno=$?
	done
	
	# Converte em lista textual enumerada.
	generos_lista=$( echo "$generos_lista" | egrep '^<td>' | sed 's/<[^>]*>//g; s/\&amp;/\&/g;' )

	local gen='' IFS=$'\n' n=0
	for gen in $generos_lista
	do
		echo -e "$n\t $gen" >> $LISTA_GEN
		let n++
	done
}

# Busca gêneros recebidos do site na lista de gêneros padrão.
function BUSCA_GEN(){
	local entrada="$1" gen IFS lista_final l1 l2

	# Converte gêneros "desconhecidos" para seus correspondentes ID3.
	entrada=$( echo "$entrada" | sed "$DICIO_GEN" )

	# Cortando e pesquisando.
	IFS='-'
	for gen in $entrada
		do
			# Grepa.
			test -n "$lista_final" && \
			lista_final="$( egrep -i "$gen" $LISTA_GEN )\n$lista_final" || \
			lista_final=$( egrep -i "$gen" $LISTA_GEN )
		done

	# Formata 2 colunas. Evita que linhas sumam quando muitas ocorrem.
	while read -t 1 -r l1
		do
			read -t 1 -r l2; printf "|%-25s\t |%s\n" "$l1" "$l2"
		done <<< "$( echo -e "$lista_final" | sort | uniq | sort -n )"
}

## Substitui espaços e remove caractere estranhos
function FIXNAME(){
	num=1;
	while true; do
		ls ${num}-* 1>/dev/null 2>/dev/null
		if [ $? -eq 0 ]; then
			file=$(ls ${num}-* 2>/dev/null)
			
			# Remover espaços
			newname=${file// /_}

			# Remover caracteres bagunceiros
			newname=${newname//[\!\@\#\$\%\&\*\?\;\(\)\[\]\]\{\}\<\>\'\"\`\\]/}

			# Remover padrão especificado em $2 ex.: album-dl --fixname "_Áudio_Oficial"
			[[ ! -z $1 ]] && newname=${newname//$1/};

			[[ "$file" != "$newname" ]] && mv "$file" "$newname" && echo "$newname";
		else
			# Sem mais arquivos
			break
		fi
		let num++
	done
}

## Renumera arquivos mp3, se playlist estiver fora da ordem do álbum.
function RENUMERA_MP3(){
	local IFS=$'\n' arqs arq narq=1 faixa faixa_ nfaixa=0 tmp lista i pto
	local dicio="áàâã|a éèêẽ|e íìîĩ|i óòôõ|o úùûũ|u ç|c"

	# Sem arquivos .mp3, não há o que renumerar.
	MP3_CHECK

	# Se não possui títulos, deve obter.
	if [ "${#titulo[@]}" -eq "0" ]; then if [ -n "$1" ]; then GET_METADATA "$1";
	else LETRAS_URL_FINDER && GET_METADATA $url_letras; fi fi

	# Listando arquivos mp3.
	# OBS: FIXNAME() pode ter alterado os nomes e grep -w pede espaços.
	arqs=$( ls *.mp3 | sort -n | cut -d'-' -f 2-9999 ); arqs=${arqs//_/ };

	# As faixas do letras.mus.br estão na ordem correta.
	for faixa in "${titulo[@]}"
		do
			# Guarda núm. da faixa e seu nome original.
			# A põe em minúsculo e remove acentos.
			let nfaixa++; faixa_=$faixa; faixa=${faixa,,*}; for i in \
			${dicio// /$'\n'}; do faixa=${faixa//[${i%|*}]/${i#*|}}; done

			# Remove metainfos de feat, part e prod.
			for i in feat part prod; do faixa=${faixa//\($i*\)/}
			faixa_=${faixa_//\($i*\)/}; done

			# Primeiro filtro, buscando bytes com ou sem acentos.
			# Lista arquivos que mais possuem, em sequência, os bytes da faixa.
			i=0; lista=''; while [ "$i" -lt "${#faixa_}" ]
				do
					lista=$lista$'\n'$( echo "$arqs" | \
					egrep -i "\\${faixa:$i:2}|\\${faixa_:$i:2}" )
					lista=$lista$'\n'$( echo "$arqs" | \
					egrep -i "\\${faixa:$i:3}|\\${faixa_:$i:3}" )
					lista=$lista$'\n'$( echo "$arqs" | \
					egrep -i "\\${faixa:$i:4}|\\${faixa_:$i:4}" )
					lista=$lista$'\n'$( echo "$arqs" | \
					egrep -i "\\${faixa:$i:5}|\\${faixa_:$i:5}" )
					let i++
				done

			# Segundo filtro, buscando palavras com ou sem acentos.
			# Os arquivos que mais possuirem as palavras da faixa.
			i=''; for i in ${faixa// /$'\n'} ${faixa_// /$'\n'}; do
			lista=$lista$'\n'$( echo -ne "$arqs" | egrep -i -w "$i" ); done

			# Limpa lista e add pontuações. Remove falso-positivos (linhas em branco).
			lista=$( echo -ne "$lista" | sort | uniq -c | sort -nr ); tmp=''
			for i in $lista; do if [ "${#i}" -gt "8" ]; then test -n "$tmp" && \
			tmp=$tmp$'\n'$i || tmp=$i; fi done; lista=$tmp

			# Terceiro filtro, descontando pontos por bytes.
			# Para subir arquivo que mesmo com nome curto apareceu mais vezes.
			tmp=''; for i in $lista; do pto=${i:0:7}; pto=${pto//[![:digit:]]/};
			test -n "$tmp" && tmp=$tmp$'\n'"$(($pto-${#i})) ${i#*  *[[:digit:]] }" || \
			tmp="$(($pto-${#i})) ${i#*  *[[:digit:]] }"; done; lista=$tmp
			lista=$( echo -ne "$lista" | sort -nr )

			# Quarto filtro. Mantendo na lista apenas as maiores pontuações.
			# Arquivos com baixa pontuação são menos prováveis.
			# OBS: Enquanto o próximo possuir até 4 pontos a menos, continua.
			pto=${lista%% *}; tmp=''; for i in $lista; do if [ "${i%% *}" -ge "$(($pto-4))" ]
			then test -n "$tmp" && tmp=$tmp$'\n'"$i" || tmp="$i"; pto=${i%% *}; fi done
			lista="$tmp"

			# Quinto filtro. Intervenção manual, se necessário.
			# Dois ou mais ou nenhum arquivos na lista, necessita de intervenção manual.
			tmp=0; for i in $lista; do let tmp++; done; if [ "$tmp" -ge 2 ] || \
			[ "$tmp" -eq 0 ]; then echo -e "$INTE Que arquivo se refere a faixa $nfaixa: \"$faixa_\"?" && \
			select arq in $arqs; do lista="1 $arq"; break 1; done; fi

			# O arquivo correspondente, por aparecer mais vezes, é o primeiro.
			read -r arq <<<$( echo -ne "$lista" ); arq=${arq#*[0-999] }
			arq=$( ls *-${arq// /_} *-$arq 2> /dev/null | uniq ); narq=${arq%%-*};

			# Se número do arquivo não é igual ao da faixa, renomeia arquivo.
			if [ -n "$arq" ]; then if [ "$narq" -ne "$nfaixa" ]; then
			echo -e "${ERRO/ERRO/INFO} Movendo \"$arq\" => \"$nfaixa-${arq#*-}\""
			mv "$arq" "$nfaixa-${arq#*-}"; else echo -e "$INFO OK => $arq"; fi fi

			# Remove de $arqs o arquivo já trabalhado.
			arq=${arq//_/ }; arq=${arq#*-}; arqs=${arqs/$arq/}
		done
}

## Encontra link do www.letras.mus.br pelo DuckDuckGo.
function LETRAS_URL_FINDER(){
	# Variáveis locais.
	local pes_album pes_artista query

	# Pede dados para busca ao usuário.
	echo -ne "$INTE Nome do álbum: "
	read pes_album
	echo -ne "$INTE Nome da banda ou artista: "
	read pes_artista

	# Montando o que pesquisar...
	query="$pes_album Discografia $pes_artista \"- LETRAS.MUS.BR\""

	# Debugzinho...
	echo -e "$INFO Pesquisando no \e[93mduckduckgo\e[0m: \e[32m$query\e[0m"

	# Realiza busca.
	resultado=$(
		wget -qO- --user-agent="$ua" "$headers" \
		"https://html.duckduckgo.com/html/?q=${query// /+}" | \
		egrep -o "http.*letras.mus.br.*discografia/[^\"]+" | uniq
	);

	# Checa se pesquisa foi concluída.
	[[ -z "$resultado" ]] && echo -e "$ERRO Problema ao procurar url";

	# Pede que o usuário escolha uma url como fonte dos metadados.
	select url_letras in $resultado "Nenhum"
	do
		if [ "$url_letras" == "Nenhum" ]
		then
			unset url_letras
			break
		elif [ ! -z "$url_letras" ]
		then
			echo -e "$INFO URL: \e[36m$url_letras\e[0m"
			break
		else
			echo -e "$ERRO Escolha uma das opções listadas"
		fi
	done
}

## Captura metadados do www.letras.mus.br
function GET_METADATA(){
	# Para padronizar em qualquer ambiente.
	local link_tmp="$1" link="$1" IFS=$'\n' n=0 tmp i dados_html="" retorno=2
	local error_msg="Passar link do albúm https://www.letras.mus.br/[Artista]/discografia/[Nome do albúm]/"

	# Filtrando domínio para conferir.
	link_tmp=${link_tmp#*.}; link_tmp=${link_tmp%%/*}

	# Deve ser um link do www.letras.mus.br ou m.letras.mus.br
	[[ "$link_tmp" != "letras.mus.br" ]] && echo "$error_msg" && return 2;

	# Obtém os metadados brutos.
	while [ "$retorno" -ne "0" ]
	do
		dados_html=$( wget -q --user-agent="$ua" "$headers" $link -O - )
		retorno=$?; test $retorno -ne 0 && echo -e "$ERRO Erro ao obter metadados :("
	done

	# Nome do artista/banda.
	artista=${dados_html#*Discografia de }; artista=${artista%% - LETRAS.MUS.BR*}

	# Nome do albúm.
	album=${dados_html#*\<title\>}; album=${album%% |*}

	# O ano vem na url.
	ano=${link##*-}; ano=${ano/\//}

	# Gênero. Se vazio, usa "other".
	genero=${dados_html##*\"genre\":\"}; genero=${genero%%\",*};
	test "${genero:0:1}" = "<" && genero="other"

	# Títulos das músicas.
	tmp=$( echo "$dados_html" | sed 's/\<li\>/\n/g' | egrep ' data-name=' )

	for i in $tmp
	do
		i=${i##*\<b\>}; i=${i%%\</b\>*}; titulo[$n]="$i"; let n++
	done

	# A capa do disco.
	# ATENÇÃO: O id3v2 do sourceforge atualmente NÃO FUNCIONA para adicionar capas!
	# Instale a versão do github, que segundo o próprio desenvolvedor está corrigida.
	# Reclamação: https://sourceforge.net/p/id3v2/patches/17/
	# Solução: https://github.com/myers/id3v2/pull/2
	tmp=""; tmp=${dados_html#*artworkModal-image\" src=\"}; tmp=${tmp%%\"*}
	capa=capa_$RANDOM.${tmp##*.} # Nome do arquivo de imagem e sua extensão.
	retorno=2
	while [ "$retorno" -ne "0" ]
	do
		wget -q --user-agent="$ua" "$headers" $tmp -O $capa
		retorno=$?; test $retorno -ne 0 && echo -e "$ERRO Erro ao baixar capa :("
	done
}

## Insere metadados nos arquivos de música.
function SET_TAGS(){
	## Sem arquivos .mp3, não há o que tagar.
	MP3_CHECK

	## Pode receber url como parâmetro ou não. Se não recebe, chama LETRAS_URL_FINDER.
	test -n "$1" && url_letras="$1" || LETRAS_URL_FINDER

	## Busca metadados sobre o albúm.
	GET_METADATA $url_letras && echo -e "$INFO Gênero registrado : \e[1;7;94m$genero\e[0m"

	## Necessário possuir a lista de gêneros ID3.
	GERA_LISTA_GENEROS

	## Pede que o usuário escolha um gênero do padrão ID3 para o albúm.
	local valido=0 genero_old=$genero
	while [ "$valido" -eq "0" ]
		do
		BUSCA_GEN $genero
		echo -ne "$INTE Escolha (número) ou digite um novo gênero: "; read id_genero

		## Valida ou não ID digitado.
		if [[ ${id_genero:0:1} = [[:digit:]] ]]
			then
				test $( BUSCA_GEN $genero | sed 's/ |/\n/g; s/|//g;' | \
				egrep --count "^$id_genero	" ) -eq 1 && valido=1
			else
				test -z "$id_genero" && genero=$genero_old || genero=$id_genero && genero_old=$genero
		fi
		done

	## Define gênero (seu nome e não mais id. Id está em $id_genero).
	genero=$( egrep "^$id_genero	" $LISTA_GEN | cut -f 2 ); genero=${genero/ /}

	## Infos gerais.
	echo -e "$INFO Artista: \e[91m$artista\e[0m"
	echo -e "$INFO Álbum  : \e[96m$album\e[0m"
	echo -e "$INFO Ano    : \e[33m$ano\e[0m"
	echo -e "$INFO Gênero : \e[94m$genero\e[0m"

	# Lista de arquivos mp3.
	local files=$( ls *.mp3 | sort -n ) faixa=1 n=0 s IFS=$'\n'

	# Imprime infos específicas.
	for s in ${files[@]}; do
		echo -e "$INFO Faixa $faixa: \e[92m${titulo[$n]}\e[0m ==> \e[93m$s\e[0m"
		let n++ faixa++
	done

	## Aplica etiquetas (usando codificação ISO-8859-1)
	read -p "Aplicar? (Sim/Não): " op
	if [[ "$op" == "Sim" || "$op" == "sim" || "$op" == "S" || "$op" == "s" ]];then
		n=0 faixa=1
		for s in ${files[@]}; do
			[[ ! -z "${titulo[$n]}" ]] && id3v2 -t "$( echo -n ${titulo[$n]} | iconv -f UTF-8 -t ISO-8859-1 )" $s;
			[[ ! -z "$artista" ]] && id3v2 -a "$( echo -n $artista | iconv -f UTF-8 -t ISO-8859-1 )" $s;
			[[ ! -z "$album" ]] && id3v2 -A "$( echo -n $album | iconv -f UTF-8 -t ISO-8859-1 )" $s;
			[[ ! -z "$id_genero" ]] && id3v2 -g "$id_genero" $s;
			[[ ! -z "$faixa" ]] && id3v2 -T "$faixa/${#titulo[@]}" $s;
			[[ ! -z "$ano" ]] && id3v2 -y "$ano" $s;
			[[ -f "$capa" ]] && id3v2 --APIC "$capa" $s; # Leia o ATENÇÃO em GET_METADATA()
			let n++ faixa++
		done

		# Imagem de capa já não é necessária.
		rm "$capa"
	fi
}

# Imprime informações.
function SHOW_TAGS(){
	local IFS=$'\n' s res artista album genero ano titulo

	# Sem arquivos, sem metadados.
	MP3_CHECK

	for s in $( ls *.mp3 | sort -n ); do
		# Coleta metadados (estão em ISO-8859-1) e filtra.
		res=$( id3v2 -l "$s" | iconv -f ISO-8859-1 -t UTF-8 ); res="${res##*.mp3:$'\n'}"
		artista="${res#*Soloist\(s\)\)\: }"; artista="${artista%%$'\n'*}"
		album="${res#* title\)\: }"; album="${album%%$'\n'*}"
		genero="${res#*Content type\): }"; genero="${genero%% \(*}"
		ano="${res#*\(Year\)\: }"; ano="${ano%%$'\n'*}"
		titulo="${res#*content description\)\: }"; titulo="${titulo%%$'\n'*}"

		# Imprime.
		echo -e "$INFO Arquivo: \e[93m$s\e[0m"
		echo -e "$INFO Título : \e[92m$titulo\e[0m"
		echo -e "$INFO Artista: \e[91m$artista\e[0m"
		echo -e "$INFO Álbum  : \e[96m$album\e[0m"
		echo -e "$INFO Ano    : \e[33m$ano\e[0m"
		echo -e "$INFO Gênero : \e[94m$genero\e[0m\n"
	done
}

# Baixar album
function DL(){
	## Recebe link da playlist, se não recebido em $1
	test -z $1 && read -p "$( echo -ne "$INTE" ) Endereço da playlist: " url || url="$1"

	## A qualidade pode ser definida entre 0 (mais alta) a 9 (mais baixa) em $2.
	test -z $2 && local qualidade=5 || local qualidade=$2

	## Recebe Instâncias Invidious e testa recebimento.
	local instancias=$(
		wget -qO- "https://api.invidious.io/instances.json?pretty=1&sort_by=health" | \
		egrep -v "\.onion|\.i2p" | egrep -o "https*://[^/\",]*" 
	)
	[[ -z "$instancias" ]] && echo -e "$ERRO Erro no recebimento de instâncias" && exit 1;

	## Põe como prioridade, entre as instâncias, o domínio da playlist.
	local instancia_on=''
	instancias=${instancias/${url%/*}$'\n'/}; instancias=${url%/*}$'\n'$instancias

	## Captura de links das músicas a partir da playlist.
	links=$( wget --no-check-certificate "$url" -qO- | egrep -o 'watch\?v=[^\"\&]*' | \
	cat -n | rev | sort | uniq -w 11 | rev | sort -n | cut -f 2 );
	[[ -z "$links" ]] && echo -e "$ERRO Erro no recebimento da playlist" && exit 1;

	num=1;
	for song in ${links[@]}; do
		local arq=$num-*.mp3; test $arq != $num'-*.mp3' && \
		echo -e "$INFO O arquivo parece já ter sido baixado: \e[93m$(ls ${num}-*.mp3)\e[0m" && let num++ && continue

		# Percorrendo instâncias.
		for instancia in $instancia_on ${instancias[@]}; do
			# O comando.
			local CMD="youtube-dl --no-call-home -x --audio-format mp3 \
			--audio-quality $qualidade -o ${num}-%(title)s.%(ext)s"

			# Debug.
			echo -e "$INFO Executando: \e[93m${CMD//[[:cntrl:]]/}\" \"\e[36m${instancia}/${song}&listen=1\e[93m\"\e[0m"

			# Tenta baixar.
			CMD="${CMD//[[:cntrl:]]/} ${instancia}/${song}&listen=1"; $CMD

			# Guarda instância funcional, se funcionar.
			if [ $? -eq 0 ];then
				instancia_on=$instancia
				break
			else
				# Se a falha foi com a instancia funcional, a descarta.
				test "$instancia" = "$instancia_on" && instancia_on=''
				echo -e "$ERRO O youtube-dl falhou"
				continue
			fi
		done

		## Depois de tentar todas as instâncias testa se o arquivo foi baixado
		if [ ! -z "$(ls ${num}-* 2>/dev/null)" ];then
			#Sucesso
			echo -e "$INFO Arquivo salvo: \e[95m$(ls $num-* 2>/dev/null)\e[0m"
		else
			#Falha
			echo -e "$ERRO Instâncias esgotadas! Verifique a sua conexão" && exit 1
		fi
		let num++
	done
}

# Checa dependências a cada execução.
DEP_CHECK

# Verifica por comandos aceitos, início.
case "$1" in
	'--dl'|'--download'|'-d') DL $2 $3 ;;
	'--fix'|'--fixname'|'-f') FIXNAME $2 ;;
	'--set'|'--set-tags'|'-s') SET_TAGS $2 ;;
	'--sh'|'--show-tags'|'-v') SHOW_TAGS ;;
	'--rn'|'--renumera'|'-r' ) RENUMERA_MP3 $2 ;;
							*) USO ;;
esac
